# 常用内建模块
# Python之所以自称“batteries included（功能齐全）”，
# 就是因为内置了许多非常有用的模块，无需额外安装和配置，即可直接使用。
# 1.datetime
# datetime是Python处理日期和时间的标准库。
from datetime import datetime

# 注意到datetime是模块，datetime模块还包含一个datetime类，通过from datetime import datetime导入的才是datetime这个类。
# 如果仅导入import datetime，则必须引用全名datetime.datetime。
# a. 获取当前日期和时间
now = datetime.now()
print(now)
print(type(now))
# b. 获取指定日期和时间
dt = datetime(2008, 2, 14, 12, 21)
print(dt)
# c. datetime转换为timestamp
# 在计算机中，时间实际上是用数字表示的。我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time，
# 记为0（1970年以前的时间timestamp为负数），当前时间就是相对于epoch time的秒数，称为timestamp。
# 即：timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00， 对应的北京时间是： timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00
# 可见timestamp的值与时区毫无关系，因为timestamp一旦确定，其UTC时间就确定了，转换到任意时区的时间也是完全确定的，
# 这就是为什么计算机存储的当前时间是以timestamp表示的，因为全球各地的计算机在任意时刻的timestamp都是完全相同的（假定时间已校准）
# 注意Python的timestamp是一个浮点数。如果有小数位，小数位表示毫秒数
# 某些编程语言（如Java和JavaScript）的timestamp使用整数表示毫秒数，这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法
print(now.timestamp())
print(dt.timestamp())
# d. timestamp转换为datetime
t = 1202962860.0
print(datetime.fromtimestamp(t))
# datetime默认为本地时间，即当前操作系统设定的时区，例如北京时区是东8区
print(datetime.fromtimestamp(now.timestamp()))
# timestamp也可以直接被转换到UTC标准时区的时间：
print(datetime.utcfromtimestamp(now.timestamp()))
# e.str转换为datetime( strptime 即为str parse time)
# 很多时候，用户输入的日期和时间是字符串
tday = datetime.strptime('2019-8-22 9:37:59', '%Y-%m-%d %H:%M:%S')
print(tday)
# f.datetime转换为str( strftime 即为str from time)
print(now.strftime('%a,%b %d %H:%M %Y'))
# g.datetime加减
# 对日期和时间进行加减实际上就是把datetime往后或往前计算，得到新的datetime。
# 加减可以直接用+和-运算符，不过需要导入timedelta这个类：
from datetime import datetime, timedelta

now = datetime.now()
print('now:', now)
print('-1d:', now - timedelta(days=1))
print('+10h:', now + timedelta(hours=10))
print('+1d+10h:', now + timedelta(days=1, hours=10))
# h.本地时间转换为UTC时间
# 一个datetime类型有一个时区属性tzinfo，但是默认为None，所以无法区分这个datetime到底是哪个时区，除非强行给datetime设置一个时区：
from datetime import datetime, timedelta, timezone

# 创建时区UTC+8:00
tz_utc_8 = timezone(timedelta(hours=8))
now = datetime.now()
print(now)
print(now.timestamp())
# 强制设置为UTC+8:00
dt = now.replace(tzinfo=tz_utc_8)
print(dt)
print(dt.timestamp())
# i.时区转换
# 可以先通过utcnow()拿到当前的UTC时间，再转换为任意时区的时间：
# 拿到UTC时间，并强制设置时区为UTC+0:00:
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
print(utc_dt)
# astimezone():将转换时区为北京时间
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
print(bj_dt)
# 东京时间
tokyo_dt = utc_dt.astimezone(timezone(timedelta(hours=9)))
print(tokyo_dt)
# 注：不是必须从UTC+0:00时区转换到其他时区，任何带时区的datetime都可以正确转换，例如上述bj_dt到tokyo_dt的转换
tokyo_dt2 = bj_dt.astimezone(timezone(timedelta(hours=9)))
print(tokyo_dt2)

# 如果要存储datetime，最佳方法是将其转换为timestamp再存储，因为timestamp的值与时区完全无关。


# 2. collections
# collections是Python内建的一个集合模块，提供了许多有用的集合类。
# a. namedtuple
# 我们知道tuple可以表示不变集合，例如，一个点的二维坐标就可以表示成：
p = (1, 2)
print(p)

# 但是，看到(1, 2)，很难看出这个tuple是用来表示一个坐标的。
# 定义一个class又小题大做了，这时，namedtuple就派上了用场：
# namedtuple是一个函数，它用来创建一个自定义的tuple对象，并且规定了tuple元素的个数，并可以用属性而不是索引来引用tuple的某个元素。
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p)
print(p.x)
print(p.y)
print(p, Point)
print(p, tuple)
Circle = namedtuple('Circle', ['x', 'y', 'r'])
c = Circle(9, 9, 4)

# b.deque
# 使用list存储数据时，按索引访问元素很快，但是插入和删除元素就很慢了，因为list是线性存储，数据量大的时候，插入和删除效率很低。
# deque是为了高效实现插入和删除操作的双向列表，适合用于队列和栈：
from collections import deque

q = deque(['a', 'b', 'c'])
print(q)
q.append('x')
q.appendleft('y')
print(q)
print(q.pop())
print(q.popleft())
print(q)

# c.defaultdict
# 使用dict时，如果引用的Key不存在，就会抛出KeyError。如果希望key不存在时，返回一个默认值，就可以用defaultdict
from collections import defaultdict

dd = defaultdict(lambda: 'N/A')
dd['key1'] = 'abc'
print(dd['key1'])
print(dd['key2'])

# d.OrderedDict
# 使用dict时，Key是无序的。在对dict做迭代时，我们无法确定Key的顺序。
# 如果要保持Key的顺序，可以用OrderedDict：
from collections import OrderedDict

d = dict([('a', 1), ('b', 2), ('c', 3)])
print(d)
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
print(od)
od2 = OrderedDict()
od2['q'] = 1
od2['w'] = 1
od2['e'] = 1
od2['r'] = 1
# OrderedDict的Key会按照插入的顺序排列，不是Key本身排序
print(list(od2.keys()))


# OrderedDict可以实现一个FIFO（先进先出）的dict，当容量超出限制时，先删除最早添加的Key：
class LastUpdatedOrderedDict(OrderedDict):

    def __init__(self, capacity):
        super(LastUpdatedOrderedDict, self).__init__()
        self._capacity = capacity

    def __setitem__(self, key, value):
        containsKey = 1 if key in self else 0
        if len(self) - containsKey >= self._capacity:
            last = self.popitem(last=False)
            print('remove:', last)
        if containsKey:
            del self[key]
            print('set:', (key, value))
        else:
            print('add:', (key, value))
        OrderedDict.__setitem__(self, key, value)


# e.ChainMap
# ChainMap可以把一组dict串起来并组成一个逻辑上的dict。ChainMap本身也是一个dict，但是查找的时候，会按照顺序在内部的dict依次查找。
# 什么时候使用ChainMap最合适？举个例子：应用程序往往都需要传入参数，参数可以通过命令行传入，可以通过环境变量传入，还可以有默认参数。
# 我们可以用ChainMap实现参数的优先级查找，即先查命令行参数，如果没有传入，再查环境变量，如果没有，就使用默认参数。
from collections import ChainMap
import os, argparse

# 构造缺省的参数
defaults = {
    'color': 'red',
    'user': 'guest'
}
# 构造命令行参数
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = {k: v for k, v in vars(namespace).items() if v}
# 组合成ChainMap
combined = ChainMap(command_line_args, os.environ, defaults)
# 打印参数
print('color=%s' % combined['color'])
print('user=%s' % combined['user'])
# 直接run：color=red，user=guest
# 命令行执行python demo12.py -u ljy -c green，结果为：color=green，user=ljy
# 同时传入命令行参数和环境变量，命令行参数的优先级较高：user=bob color=blue python demo12.py -u emily

# f.Counter
# 一个简单的计数器,例如，统计字符出现的个数：
from collections import Counter

c = Counter()
for ch in 'qwerty bob aaa':
    c[ch] += 1
print(c)

# 3. base64
# Base64是一种用64个字符来表示任意二进制数据的方法。
# Base64的原理很简单，首先，准备一个包含64个字符的数组：
# ['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
# 然后，对二进制数据进行处理，每3个字节一组，一共是3x8=24bit，划为4组，每组正好6个bit
# 这样我们得到4个数字作为索引，然后查表，获得相应的4个字符，就是编码后的字符串。
# 所以，Base64编码会把3字节的二进制数据编码为4字节的文本数据，长度增加33%，好处是编码后的文本数据可以在邮件正文、网页等直接显示。
# 如果要编码的二进制数据不是3的倍数，最后会剩下1个或2个字节怎么办？Base64用\x00字节在末尾补足后，再在编码的末尾加上1个或2个=号，表示补了多少字节，解码的时候，会自动去掉。
import base64

a = base64.b64encode(b'emily')
print(a)
b = base64.b64decode(a)
print(b)
# 由于标准的Base64编码后可能出现字符+和/，在URL中就不能直接作为参数，所以又有一种"url safe"的base64编码，其实就是把字符+和/分别变成-和_：
s = b'i\xb7\x1d\xfb\xef\xff'
c = base64.b64encode(s)
print(c)
d = base64.urlsafe_b64encode(s)
print(d)
e = base64.urlsafe_b64decode(d)
print(e)
# 还可以自己定义64个字符的排列顺序，这样就可以自定义Base64编码，不过，通常情况下完全没有必要。
# Base64是一种通过查表的编码方法，不能用于加密，即使使用自定义的编码表也不行。
# Base64适用于小段内容的编码，比如数字证书签名、Cookie的内容等。
# 由于=字符也可能出现在Base64编码中，但=用在URL、Cookie里面会造成歧义，所以，很多Base64编码后会把=去掉：
# # 标准Base64: 'abcd' -> 'YWJjZA==', 自动去掉=: 'abcd' -> 'YWJjZA'
# 去掉=后怎么解码呢？因为Base64是把3个字节变为4个字节，所以，Base64编码的长度永远是4的倍数，因此，
# 需要加上=把Base64字符串的长度变为4的倍数，就可以正常解码了。


# 4.struct
# bytes和其他二进制数据类型的转换
# struct的pack函数把任意数据类型变成bytes：
import struct

a = struct.pack('>I', 10240099)
print(a)
# pack的第一个参数是处理指令，'>I'的意思是：
# >表示字节顺序是big-endian，也就是网络序，I表示4字节无符号整数。
# 后面的参数个数要和处理指令一致。

# unpack把bytes变成相应的数据类型：
b = struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')
# 根据>IH的说明，后面的bytes依次变为I：4字节无符号整数和H：2字节无符号整数。
print(b)
# Windows的位图文件（.bmp）是一种非常简单的文件格式，我们来用struct分析一下
# 首先找一个bmp文件，读入前30个字节来分析：
try:
    dir_path = os.path.join(".", "file")
    # file_path = os.path.join(dir_path, 'pic01.bmp')
    file_path = os.path.join(dir_path, 'pic02.bmp')
    f = open(file_path, 'rb')
    s = f.read(30)
finally:
    f.close()
# s=base64.b64decode(s)
print(s)


def bmp_info(data):
    s = struct.unpack('<ccIIIIIIHH', data)

    if s[1] == b'M':
        print('该文件是位图文件。')
        return {
            'width': s[6],
            'height': s[7],
            'color': s[9],
        }
    else:
        print('该文件不是位图文件。')


r = bmp_info(s)
print(r)

# 5. hashlib
# Python的hashlib提供了常见的摘要算法，如MD5，SHA1等等。
# 摘要算法又称哈希算法、散列算法。它通过一个函数，把任意长度的数据转换为一个长度固定的数据串（通常用16进制的字符串表示）。
# 目的是为了发现原始数据是否被人篡改过。
# 要算法之所以能指出数据是否被篡改过，就是因为摘要函数是一个单向函数，计算f(data)很容易，但通过digest反推data却非常困难。
# 而且，对原始数据做一个bit的修改，都会导致计算出的摘要完全不同。
# MD5是最常见的摘要算法，速度很快，生成结果是固定的128 bit字节，通常用一个32位的16进制字符串表示。
import hashlib

md5 = hashlib.md5()
md5.update('人生苦短，我用python'.encode('utf-8'))
print(md5.hexdigest())
md51 = hashlib.md5()
md51.update('人生苦短，我用python.'.encode('utf-8'))
print(md51.hexdigest())
# 如果数据量很大，可以分块多次调用update()，最后计算的结果是一样的：
md52 = hashlib.md5()
md52.update('人生苦短，'.encode('utf-8'))
md52.update('我用python'.encode('utf-8'))
print(md52.hexdigest())

# 另一种常见的摘要算法是SHA1，
# SHA1的结果是160 bit字节，通常用一个40位的16进制字符串表示。
sha1 = hashlib.sha1()
sha1.update('人生苦短，我用python'.encode('utf-8'))
print(sha1.hexdigest())
sha11 = hashlib.sha1()
sha11.update('人生苦短，我用python.'.encode('utf-8'))
print(sha11.hexdigest())
sha12 = hashlib.sha1()
sha12.update('人生苦短，'.encode('utf-8'))
sha12.update('我用python'.encode('utf-8'))
print(sha12.hexdigest())
# 比SHA1更安全的算法是SHA256和SHA512，不过越安全的算法不仅越慢，而且摘要长度更长。

# 有没有可能两个不同的数据通过某个摘要算法得到了相同的摘要？完全有可能，
# 因为任何摘要算法都是把无限多的数据集合映射到一个有限的集合中。这种情况称为碰撞

# 摘要算法应用：一个常见的使用场景就是数据库中保存用户的用户名和密码，防止被拖库后泄露用户信息


# 6. hmac
# 通过哈希算法，我们可以验证一段数据是否有效，方法就是对比该数据的哈希值
# 为了防止黑客通过彩虹表根据哈希值反推原始口令，在计算哈希的时候，不能仅针对原始输入计算，
# 需要增加一个salt来使得相同的输入也能得到不同的哈希，这样，大大增加了黑客破解的难度。
# 如果salt是我们自己随机生成的，通常我们计算MD5时采用md5(message + salt)。
# 但实际上，把salt看做一个“口令”，加salt的哈希就是：计算一段message的哈希时，根据不通口令计算出不同的哈希。要验证哈希值，必须同时提供正确的口令
# 这实际上就是Hmac算法：Keyed-Hashing for Message Authentication。
# 它通过一个标准算法，在计算哈希的过程中，把key混入计算过程中。
# 和我们自定义的加salt算法不同，Hmac算法针对所有哈希算法都通用，无论是MD5还是SHA-1。
# 采用Hmac替代我们自己的salt算法，可以使程序算法更标准化，也更安全。
import hmac

# 需要注意传入的key和message都是bytes类型，str类型需要首先编码为bytes
message = b'Hello, world!'
key = b'ljy_key'
h = hmac.new(key, message, digestmod='MD5')
print(h.hexdigest())

# 7.itertools
import itertools

# itertools提供了非常有用的用于操作迭代对象的函数
# 首先，我们看看itertools提供的几个“无限”迭代器：
# a. ount()会创建一个无限的迭代器，所以下面代码会打印出自然数序列，根本停不下来，只能按Ctrl+C退出
natuals = itertools.count(1)
# for n in natuals:
#     print(n)

# b.cycle()会把传入的一个序列无限重复下去
cs = itertools.cycle('abc')  # 注意字符串也是序列的一种
# for c in cs:
#     print(c)

# c.repeat()负责把一个元素无限重复下去，不过如果提供第二个参数就可以限定重复次数
ns = itertools.repeat('A', 3)
for n in ns:
    print(n)
# 无限序列只有在for迭代时才会无限地迭代下去，如果只是创建了一个迭代对象，它不会事先把无限个元素生成出来，
# 事实上也不可能在内存中创建无限多个元素。
# 无限序列虽然可以无限迭代下去，但是通常我们会通过takewhile()等函数根据条件判断来截取出一个有限的序列：
natuals = itertools.count(1)
ns = itertools.takewhile(lambda x: x <= 10, natuals)
print(list(ns))

# d.chain()
# 可以把一组迭代对象串联起来，形成一个更大的迭代器：
for c in itertools.chain('abc', 'def'):
    print(c)
# e.groupby()
# 把迭代器中相邻的重复元素挑出来放在一起：
for key, group in itertools.groupby('aaabbbccaaa'):
    print(key, list(group))
# 实际上挑选规则是通过函数完成的，只要作用于函数的两个元素返回的值相等，这两个元素就被认为是在一组的，而函数返回值作为组的key。
# 如果我们要忽略大小写分组，就可以让元素'A'和'a'都返回相同的key：
for key, group in itertools.groupby('aAAbBbcCAAa', lambda c: c.upper()):
    print(key, list(group))

    # 8.contextlib
    # 在Python中，读写文件这样的资源要特别注意，必须在使用完毕后正确关闭它们。
    # 正确关闭文件资源的一个方法是使用try...finally，但是写try...finally非常繁琐。
    # Python的with语句允许我们非常方便地使用资源，而不必担心资源没有关闭：
dir_path = os.path.join(".", "file")
file_path = os.path.join(dir_path, 'pic02.bmp')
with open(file_path, 'rb') as f:
    f.read(30)


# 并不是只有open()函数返回的fp对象才能使用with语句。实际上，任何对象，只要正确实现了上下文管理，就可以用于with语句。
# 实现上下文管理是通过__enter__和__exit__这两个方法实现的。例如，下面的class实现了这两个方法：
class Query(object):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print('begin')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print('error')
        else:
            print('end')

    def query(self):
        print('query info about %s...' % self.name)


# 这样我们就可以把自己写的资源对象用于with语句：
with Query('ljy') as q:
    q.query()

# @contextmanager
# 编写__enter__和__exit__仍然很繁琐，因此Python的标准库contextlib提供了更简单的写法，上面的代码可以改写如下：
from contextlib import contextmanager


class Query2(object):
    def __init__(self, name):
        self.name = name

    def query(self):
        print('query info about %s...' % self.name)


@contextmanager
def create_query(name):
    print('begin')
    q = Query2(name)
    yield q
    print('end')


with create_query('ljy') as q:
    q.query()


# 很多时候，我们希望在某段代码执行前后自动执行特定代码，也可以用@contextmanager实现。例如：
@contextmanager
def tag(name):
    print('<%s>' % name)
    yield  # yield调用会执行with语句内部的所有语句，因此打印出hello和world
    print('</%s>' % name)


with tag('h1'):
    print('hello')
    print('world')
# 因此，@contextmanager让我们通过编写generator来简化上下文管理。

# @closing
# 如果一个对象没有实现上下文，我们就不能把它用于with语句。这个时候，
# 可以用closing()来把该对象变为上下文对象。例如，用with语句使用urlopen()：
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)
# closing也是一个经过@contextmanager装饰的generator，这个generator编写起来其实非常简单：
# @contextmanager
# def closing(thing):
#     try:
#         yield thing
#     finally:
#         thing.close()
# 它的作用就是把任意对象变为上下文对象，并支持with语句。


# 9.urllib
# urllib提供了一系列用于操作URL的功能。
# a. Get
# urllib的request模块可以非常方便地抓取URL内容，也就是发送一个GET请求到指定的页面，然后返回HTTP的响应：
# 例如，对一个URL进行抓取，并返回响应：
from urllib import request

with request.urlopen('https://www.baidu.com/') as f:
    data = f.read()
    print('>>>>>>>status', f.status, f.reason)
    for k, v in f.getheaders():
        print('>>>>>>>%s: %s' % (k, v))
    print('>>>>>>Data:', data.decode('utf-8'))
# 如果我们要想模拟浏览器发送GET请求，就需要使用Request对象，通过往Request对象添加HTTP头，
# 我们就可以把请求伪装成浏览器。例如，模拟iPhone 6去请求豆瓣首页：
req = request.Request('http://www.douban.com/')
req.add_header('User-Agent',
               'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
    print('>>>>>>>status', f.status, f.reason)
    for k, v in f.getheaders():
        print('>>>>>>>%s: %s' % (k, v))
    print('>>>>>>Data:', f.read().decode('utf-8'))

# b.POST
# 如果要以POST发送一个请求，只需要把参数data以bytes形式传入。
# 我们模拟一个微博登录，先读取登录的邮箱和口令，然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入：
from urllib import request, parse

print('login to weibo.cn...')
email = input('Email: ')
password = input('password:')
login_data = parse.urlencode([
    ('username', email),
    ('password', password),
    ('entry', 'mweibo'),
    ('client_id', ''),
    ('savestate', '1'),
    ('ec', ''),
    ('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])
req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent',
               'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer',
               'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')
with request.urlopen(req, data=login_data.encode('utf-8')) as f:
    print('>>>>>>>status', f.status, f.reason)
    for k, v in f.getheaders():
        print('>>>>>>>%s: %s' % (k, v))
    print('>>>>>>Data:', f.read().decode('utf-8'))

# c. Handler
# 如果还需要更复杂的控制，比如通过一个Proxy去访问网站，我们需要利用ProxyHandler来处理，示例代码如下：
# import urllib
#
# proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})
# proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
# proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
# opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)
# with opener.open('http://www.example.com/login.html') as f:
#     pass

# 10.XML
# XML虽然比JSON复杂，在Web中应用也不如以前多了，不过仍有很多地方在用，所以，有必要了解如何操作XML。
# DOM vs SAX:操作XML有两种方法：DOM和SAX。
# DOM会把整个XML读入内存，解析为树，因此占用内存大，解析慢，优点是可以任意遍历树的节点。
# SAX是流模式，边读边解析，占用内存小，解析快，缺点是我们需要自己处理事件。
# 正常情况下，优先考虑SAX，因为DOM实在太占内存。
# 在Python中使用SAX解析XML非常简洁，通常我们关心的事件是start_element，end_element和char_data，
# 准备好这3个函数，然后就可以解析xml了。
from xml.parsers.expat import ParserCreate


class DefaultSaxHandler(object):
    def start_element(self, name, attrs):
        print('sax:start_element: %s, attrs: %s' % (name, str(attrs)))

    def end_element(self, name):
        print('sax:end_element: %s' % name)

    def char_data(self, text):
        print('sax:char_data: %s' % text)


xml = r'''<?xml version="1.0"?>
<ol>
    <li><a href="/python">Python</a></li>
    <li><a href="/ruby">Ruby</a></li>
</ol>
'''
handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)
# 需要注意的是读取一大段字符串时，CharacterDataHandler可能被多次调用，所以需要自己保存起来，在EndElementHandler里面再合并。
# 除了解析XML外，如何生成XML呢？99%的情况下需要生成的XML结构都是非常简单的，因此，最简单也是最有效的生成XML的方法是拼接字符串：
# def createXml():
#     L = []
#     L.append(r'<?xml version="1.0"?>')
#     L.append(r'<root>')
#     L.append(encode('some & data'))
#     L.append(r'</root>')
#     return ''.join(L)


# 11. HTMLParser
# 如果我们要编写一个搜索引擎，第一步是用爬虫把目标网站的页面抓下来，第二步就是解析该HTML页面，看看里面的内容到底是新闻、图片还是视频。
# 假设第一步已经完成了，第二步应该如何解析HTML呢？
# HTML本质上是XML的子集，但是HTML的语法没有XML那么严格，所以不能用标准的DOM或SAX来解析HTML。
# 好在Python提供了HTMLParser来非常方便地解析HTML，只需简单几行代码：
from html.parser import HTMLParser
from html.entities import name2codepoint


class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print('<%s>' % tag)

    def handle_endtag(self, tag):
        print('</%s>' % tag)

    def handle_startendtag(self, tag, attrs):
        print('<%s/>' % tag)

    def handle_data(self, data):
        print(data)

    def handle_comment(self, data):
        print('<!--', data, '-->')

    def handle_entityref(self, name):
        print('&%s;' % name)

    def handle_charref(self, name):
        print('&#%s;' % name)


print('---------htmlParser-------------')
parser = MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser -->
    <p>Some <a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')
# feed()方法可以多次调用，也就是不一定一次把整个HTML字符串都塞进去，可以一部分一部分塞进去。
# 特殊字符有两种，一种是英文表示的&nbsp;，一种是数字表示的&#1234;，这两种字符都可以通过Parser解析出来。
# 利用HTMLParser，可以把网页中的文本、图像等解析出来。
